/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.login;

import com.Ostermiller.util.RandPass;
import com.codemagi.login.model.*;
import com.codemagi.login.policy.IPasswordPolicy;
import com.codemagi.servlets.*;
import com.codemagi.servlets.validation.*;
import com.codemagi.util.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.log4j.*;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.QueryResults;

/**
 * Class extending the <code>HttpServlet</code> to implement
 * the controller for login/logout actions.
 * <p>
 * Please see the related documentation for more detailed
 * information on process flow and functionality.
 *
 * @version 1.0
 * @author August Detlefsen for CodeMagi, Inc.
 */
public class LoginController extends com.codemagi.servlets.NavigationController {

    //setup logger to view debug output
    static Logger log = Logger.getLogger("com.codemagi.login.LoginController");

    //MEMBERS
    protected String groupClass   = "com.codemagi.login.model.Group";
    protected String adminEmail   = "";
    protected String SMTP_HOST    = "localhost";

    //views specific to login
    protected static String VIEW_MAIN                    = "/login/index.jsp";
    protected static String VIEW_LOGIN                   = "/login/index.jsp";
    protected static String VIEW_ADMIN_USERS             = "/login/admin_users.jsp";
    protected static String VIEW_MODIFY_USER             = "/login/admin_users_modify.jsp";
    protected static String VIEW_ACCESS_DENIED           = "/login/access_denied.jsp";
    protected static String VIEW_GROUP_USERS             = "/login/admin_group_users.jsp";
    protected static String VIEW_RECOVER_FORM            = "/login/form_recover.jsp";
    protected static String VIEW_RECOVER_SUCCESS         = "/login/recover_success.jsp";
    protected static String VIEW_RESET_FORM              = "/login/form_reset.jsp";
    protected static String VIEW_RESET_SUCCESS           = "/login/reset_success.jsp";

    //messages specific to login
    protected static String MSG_USER_ADDED           = "New user created. Thank you for adding any additional user information.";
    protected static String MSG_USER_ADDED_GROUP     = "Users added to group.";
    protected static String MSG_USER_REMOVED_GROUP   = "Users removed from group.";
    protected static String MSG_PREEXISTING_USERNAME = "That username is already in use. Please choose another.";
    protected static String MSG_INVALID_USER         = "User does not exist.";
    protected static String MSG_BAD_RECOVERY_PARAMS  = "The username and email address you entered do not match any valid accounts.";
    protected static String MSG_RECOVERY_SUCCESS     = "You password has been reset and a new password has been sent to the email address on record.";
    protected static String MSG_RESET_SUCCESS        = "Password reset successfully. A new password has been emailed to the user.";

    /**
     * List containing password policy checks to run on new or changed passwords. 
     * Add PasswordPolicy objects to this list in your init() method. 
     */
    protected ArrayList<IPasswordPolicy> passwordPolicyChecks = new ArrayList<IPasswordPolicy>();


    /**                                                                                                                       
     * Initializes the servlet when it is loaded by the                                                                     
     * servlet engine.                                                                                                       
     *                                                                                                                             
     * @param config the configuration as <code>ServletConfig</code>                                                        
     *                                                                                                                        
     * @throws ServletException if initialization fails.                                                                  
     */
    public void init(ServletConfig config)
        throws ServletException {

        super.init(config);

        //get params from config files --------------------------------                                            

        //email address of administrator                                                                            
        String pAdminEmail = config.getInitParameter("adminEmail");
        if (!Utils.isEmpty(pAdminEmail)) adminEmail = pAdminEmail;

        //mail host for email                                       
        String pMailhost = application.getInitParameter("mailhost");
        if (!Utils.isEmpty(pMailhost)) SMTP_HOST = pMailhost;
        log.info("Setting mailhost to: " + SMTP_HOST);

	//define classes that this servlet will provide navigation for
	typeClasses.put("user", com.codemagi.login.model.User.class);
        typeViews.put("user", VIEW_MODIFY_USER);
        typeNames.put("user", "editUser");

        typeClasses.put("group", com.codemagi.login.model.Group.class);
        typeViews.put("group", VIEW_GROUP_USERS);
        typeNames.put("group", "group");

	//set site-wide variables
	setOnce("user.recoverPassword.email.body", "Hello,\n\nYour password to ${serverName} has been reset at your request.\n\nYou may now login using the new password: ${newPassword} \n\nIf you think you have received this message in error, please contact the site administrator at once.");

	//create validators
        IValidator integerValidator = new IntegerValidator();
        IValidator booleanValidator = new BooleanValidator();
        IValidator dateValidator = new DateValidatorUS();
        IValidator stripHtmlValidator = new StripHtmlValidator();
        IValidator notNullValidator = new NotNullValidator();
        IValidator emailAddressValidator = new EmailAddressValidator();
        IValidator stringLength8Validator = new StringLengthValidator(8);
        IValidator stringLength32Validator = new StringLengthValidator(32);
        IValidator stringLength64Validator = new StringLengthValidator(64);
        IValidator stringLength128Validator = new StringLengthValidator(128);
        IValidator[] int_notNull_validators =
            { integerValidator, notNullValidator };
        IValidator[] strip_8_validators =
            { stripHtmlValidator, stringLength8Validator };
        IValidator[] strip_32_validators =
            { stripHtmlValidator, stringLength32Validator };
        IValidator[] strip_128_validators =
            { stripHtmlValidator, stringLength128Validator };
        IValidator[] strip_notNull_64_validators =
            { stripHtmlValidator, notNullValidator, stringLength64Validator };
        IValidator[] strip_notNull_128_validators =
            { stripHtmlValidator, notNullValidator, stringLength128Validator };

        //add input validators
        addValidator("modifyUser", "returnPage", stripHtmlValidator);
        addValidator("modifyUser", "user_id", integerValidator);
        addValidator("modifyUser", "username", strip_notNull_64_validators);
        addValidator("modifyUser", "prefix", strip_8_validators);
        addValidator("modifyUser", "firstName", strip_notNull_128_validators);
        addValidator("modifyUser", "middleName", strip_128_validators);
        addValidator("modifyUser", "lastName", strip_notNull_128_validators);
        addValidator("modifyUser", "suffix", strip_8_validators);
        addValidator("modifyUser", "title", strip_128_validators);
        addValidator("modifyUser", "company", strip_128_validators);
        addValidator("modifyUser", "email", strip_notNull_128_validators);
        addValidator("modifyUser", "email", emailAddressValidator);
        addValidator("modifyUser", "phone", strip_32_validators);
        addValidator("modifyUser", "extension", strip_32_validators);
        addValidator("modifyUser", "fax", strip_32_validators);
        addValidator("modifyUser", "mobile", strip_32_validators);
        addValidator("modifyUser", "address1", strip_128_validators);
        addValidator("modifyUser", "address2", strip_128_validators);
        addValidator("modifyUser", "address3", strip_128_validators);
        addValidator("modifyUser", "city", strip_128_validators);
        addValidator("modifyUser", "state", strip_128_validators);
        addValidator("modifyUser", "zipCode", strip_32_validators);
        addValidator("modifyUser", "country", strip_128_validators);
        addValidator("modifyUser", "online", booleanValidator);
        addValidator("modifyUser", "expires", dateValidator);

        addValidator("modifyGroup", "groupId", int_notNull_validators);

        addValidator("recoverPassword", "username", strip_notNull_64_validators);
        addValidator("recoverPassword", "email", strip_notNull_128_validators);
        addValidator("recoverPassword", "email", emailAddressValidator);

        addValidator("resetPassword", "id", int_notNull_validators);

        addValidator("addToGroup", "groupId", int_notNull_validators);

        addValidator("removeFromGroup", "groupId", int_notNull_validators);

        addValidator("addUser", "username", strip_notNull_64_validators);
        addValidator("addUser", "pass1", notNullValidator);
        addValidator("addUser", "pass2", notNullValidator);

	addValidator("login", "start_in", stripHtmlValidator);
        addValidator("login", "username", strip_notNull_64_validators);
        addValidator("login", "password", notNullValidator);

	addValidator("logout", "start_in", stripHtmlValidator);

        addValidator("switchUser", "userId", int_notNull_validators);

    }


    /**
     * Handles the incoming requests.
     * <p>
     * This implementation ensures session
     * existence, retrieves the <code>acton</code>
     * and <code>todo</code> parameters, and dispatches all
     * valid actions to the target dispatchers.
     * <p>
     * The flow of the process is described in the related
     * documentation.
     * <p>
     * Application related errors/exceptions are handled
     * by forwarding the request to an error page, or the actual page
     * in case of an inlined error.
     *
     * @param req a reference to the actual <code>HttpServletRequest</code>
     *        instance.
     * @param res a reference to the actual <code>HttpServletResponse</code>
     *        instance.
     *
     * @throws ServletException if servlet related operations fail.
     * @throws IOException if i/o operations fail.
     */
    public void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

        String action  = request.getParameter("acton");
        String command = request.getPathInfo();
        log.debug("LoginController: ACTON: " + action + " COMMAND: " + command);

	//	logRequestParameters(request);
	//	logRequestAttributes(request);

	//next page to forward to will depend on the outcome of the request dispatchers   
        String nextPage = VIEW_LOGIN;

	try {
	    //dispatch request actions to the proper method
	    if ("login".equals(action)) {
		nextPage = dispatchLogin(request,response);
		
	    } else if ("logout".equals(action)) {
		nextPage = dispatchLogout(request,response);
		
	    } else if ("modifyUser".equals(action)) {
		nextPage = dispatchModifyUser(request,response);
		
	    } else if ("addUser".equals(action)) {
		nextPage = dispatchAddUser(request,response);
		
	    } else if ("modifyGroup".equals(action)) {
		
		//is it an add or remove? 
		if ( !Utils.isEmpty(request.getParameter("addToGroup")) ) {
		    nextPage = dispatchAddToGroup(request,response);
		} else if ( !Utils.isEmpty(request.getParameter("removeFromGroup")) ) {
		    nextPage = dispatchRemoveFromGroup(request,response);
		}
		
	    } else if ("users".equals(action)) {
		nextPage = dispatchUsers(request,response);

            } else if ("switchUser".equals(action)) {
                nextPage = dispatchSwitchUser(request,response);

            } else if ("recoverPassword".equals(action)) {
                nextPage = dispatchRecoverPassword(request,response);

            } else if ("resetPassword".equals(action)) {
                nextPage = dispatchResetPassword(request,response);

	    } else if ( !Utils.isEmpty(command) ) {
                nextPage = dispatchCommand(request, response);

            }

        } catch (AccessException ae) {
            nextPage = handleAccessException(request, response, ae);

        } catch (LoginException le) {
            nextPage = handleLoginException(request, response, le);

        }

        //forward                                                                         
        log.debug("Forwarding to: " + nextPage);
	if ( !VIEW_NONE.equals(nextPage) ) {
	    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}
    }


    //DISPATCHERS -------------------------------------------------------------

    /**                                                                                                                            
     * Dispatches <code>recoverPassword</code> actions.                                                                            
     */
    protected String dispatchRecoverPassword(HttpServletRequest request, HttpServletResponse response) 
	throws AccessException {

        //get the session                                                                                                          
        HttpSession session = request.getSession();

        log.debug("***** dispatchRecoverPassword *****");

        //determine the next page to route to if successful                                                              
        String nextPage = VIEW_RECOVER_SUCCESS;

        Database db = null;

        try {
            //see if there is a User bean already in session                                                                  
            IUser user = getUser(request); //(IUser)session.getAttribute("user");

            //see if they are already auth'd                                                                                     
            if (user != null && user.isAuthenticated()) {
                //if the user is already auth'd why are they requesting a password?                                          
                return VIEW_ROOT;
            }

            //if this is an actual submit, perform the edits, otherwise just forward to the editing page                    
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit)) {
                nextPage = VIEW_RECOVER_FORM;

            } else {

		//perform CSRF token test
		checkToken(request);

                //get request params
                String username = getString("username", request);
                String email    = getString("email", request);

                //did we get any validation errors?
                checkValidation(request);

                //message fields
                email = StringUtils.toLowerCase(email);

                //check for a user with the passed attributes
                db = jdo.getDatabase();
                db.begin();

                OQLQuery oql = db.getOQLQuery("SELECT u FROM " + userClass + " u " +
                                              " WHERE u.username LIKE $1 " +
                                              " AND LOWER(u.email) LIKE $2 ");
                oql.bind( username );
                oql.bind( email );

                QueryResults results = oql.execute();
                IUser modUser = null;
                if ( results.hasMore() ) {
                    modUser = (IUser)results.next();

                } else {
                    throw new AppException(MSG_BAD_RECOVERY_PARAMS);

                }

                //check for disabled user                                                                                 
                if ( !modUser.getOnline().booleanValue() ) {
                    throw new AppException(MSG_USER_DISABLED);
                }

                //check for expired user                                                                                   
                if ( modUser.getDateExpires() != null && ( System.currentTimeMillis() > modUser.getDateExpires().getTime() ) ) {
                    throw new AppException(MSG_USER_EXPIRED);
                }

                //create a new random password                                                                                
                RandPass gen   = new RandPass();  //default non-confusing alphabet                                       
                String newPass = gen.getPass();   //default length (8)                                                        

                modUser.setNewPassword(newPass);

                //set the force password change bit                                                                       
                modUser.setUpdateRequired(true);

		//remove any locks on the account                             
                modUser.setFailedLogins(0);
                modUser.setLockedUntil(null);

                db.commit();

                //send an email to the user informing them of their new password                                          
		String messageBody = get("user.recoverPassword.email.body");
		messageBody = StringUtils.replace(messageBody, "${name}", modUser.getName());
		messageBody = StringUtils.replace(messageBody, "${serverName}", getServerName(request));
		messageBody = StringUtils.replace(messageBody, "${newPassword}", newPass);

                boolean success = EmailUtils.sendEmail(adminEmail,
						       email,
						       "Password Reset",
						       messageBody,
						       SMTP_HOST);
		
		log.debug("Email sent successfully? " + success);

                nextPage = VIEW_RECOVER_SUCCESS;
            }

        } catch (AppException se) {
            log.debug("", se);

            request.setAttribute("error_message", se.getMessage());

            nextPage = VIEW_RECOVER_FORM;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_RECOVER_FORM;

        } finally {
            closeJDODatabase(db);
        }

        return nextPage;
    }


    /**                                                                                                                            
     * Dispatches <code>resetPassword</code> actions: Superuser resets a user's password
     */
    protected String dispatchResetPassword(HttpServletRequest request, HttpServletResponse response) 
	throws LoginException, AccessException {

        log.debug("***** dispatchResetPassword *****");

        //determine the next page to route to if successful                                                              
        String nextPage = VIEW_RESET_SUCCESS;

        Database db = null;

        try {
            //check for access
            IUser user = checkPermission(request, ACCESS_SUPERUSER);

	    //get params from form
	    Integer userId = getInteger("id", request);

	    //check for validation erorrs
	    checkValidation(request);

	    //load the user to reset
	    db = jdo.getDatabase();
	    db.begin();

	    IUser editUser = (IUser)db.load(Class.forName(userClass), userId);
	    request.setAttribute("editUser", editUser);

            //if this is an actual submit, perform the edits, otherwise just forward to the editing page                    
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit)) {
                nextPage = VIEW_RESET_FORM;

            } else {
		//perform CSRF token test
		checkToken(request);

                //check for disabled user                                                                                 
                if ( !editUser.getOnline().booleanValue() ) {
                    throw new AppException(MSG_USER_DISABLED);
                }

                //check for expired user                                                                                   
                if ( editUser.getDateExpires() != null && ( System.currentTimeMillis() > editUser.getDateExpires().getTime() ) ) {
                    throw new AppException(MSG_USER_EXPIRED);
                }

                //create a new random password                                                                                
                RandPass gen   = new RandPass();  //default non-confusing alphabet                                       
                String newPass = gen.getPass();   //default length (8)                                                        

                editUser.setNewPassword(newPass);

                //set the force password change bit                                                                       
                editUser.setUpdateRequired(true);

		//reset failed logins
		editUser.setFailedLogins(0);
		editUser.setLockedUntil(null);

                db.commit();

                //send an email to the user informing them of their new password                                          
                StringBuffer messageBody = new StringBuffer(512);
                messageBody
                    .append("Hello,\n\n")
                    .append("Your password to ")
		    .append( getServerName(request) )
		    .append(" has been reset by a site administrator.\n\n")
                    .append("You may now login using the new password: " + newPass + "\n\n")
                    .append("If you think you have received this message in error, please contact the site administrator at once.");

                boolean success = EmailUtils.sendEmail(adminEmail,
						       editUser.getEmail(),
						       "Password Reset",
						       messageBody,
						       SMTP_HOST);
		
		log.debug("Email sent successfully? " + success);

		//success!
                nextPage = VIEW_RESET_SUCCESS;
		request.setAttribute("error_message", MSG_RESET_SUCCESS);

		//null out the edit user in request
		request.setAttribute("editUser", null);

            }

        } catch (AppException se) {
            log.debug("", se);

            request.setAttribute("error_message", se.getMessage());

            nextPage = VIEW_RESET_FORM;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_RESET_FORM;

	} catch (ClassNotFoundException cnfe) {
	    log.debug(userClass + " not found", cnfe);
	    request.setAttribute("error_message", MSG_UNKNOWN_ERROR);

        } finally {
            closeJDODatabase(db);
        }

        return nextPage;
    }


    /**
     * Dispatches <code>addToGroup</code> actions: adds users to the selected group
     */
    protected String dispatchAddToGroup(HttpServletRequest request, HttpServletResponse response)
	throws LoginException, AccessException {
        
	log.debug("***** dispatchAddToGroup *****");

	String nextPage = VIEW_GROUP_USERS;

        Database db = null;

	try {

            //make sure user is authenticated and SU
            checkPermission(request, ACCESS_SUPERUSER);

	    //perform CSRF token test
	    checkToken(request);

	    //get data from form 
	    Integer groupId     = getInteger("groupId", request);
	    Collection addUsers = FormUtils.getSelectValues(request, "unassigned");

	    //did we get any validation errors?
	    checkValidation(request);

	    //get the db instance
            db = getJDODatabase();
            db.begin();

	    //load the group to process
	    Group group = (Group)db.load(Class.forName(groupClass), groupId);

	    //set the group back into request in case we get any errors
	    request.setAttribute("group", group);

	    //iterate the selected names, adding them to the group
	    //this is done 'backwards' ie, the Group is added to the User
	    Iterator i = addUsers.iterator();
	    while (i.hasNext() ) {
		Integer userId  = convertInteger( (String)i.next() );

		IUser user = (IUser)db.load(Class.forName(userClass), userId);

		user.addGroup(group);
	    }

	    db.commit();

	    //success!
	    request.setAttribute("error_message", MSG_USER_ADDED_GROUP);

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} catch (ClassNotFoundException cnfe) {
	    log.debug(groupClass + " not found", cnfe);

	    request.setAttribute("error_message", MSG_UNKNOWN_ERROR);

        } finally {
	    closeJDODatabase(db);
        }

        return nextPage;
    }


    /**
     * Dispatches <code>users</code> actions: adds users to the selected group
     */
    protected String dispatchUsers(HttpServletRequest request, HttpServletResponse response)
	throws LoginException, AccessException {
        
	log.debug("***** dispatchUsers *****");

	String nextPage = VIEW_ADMIN_USERS;

	//check for login
	IUser user = checkLogin(request);

        return nextPage;
    }


    /**
     * Dispatches <code>removeFromGroup</code> actions: adds users to the selected group
     */
    protected String dispatchRemoveFromGroup(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	log.debug("***** dispatchRemoveFromGroup *****");

	String nextPage = VIEW_GROUP_USERS;

        Database db = null;

	try {

            //make sure user is authenticated and SU
            checkPermission(request, ACCESS_SUPERUSER);

	    //perform CSRF token test
	    checkToken(request);

	    //get data from form 
	    Integer groupId     = getInteger("groupId", request);
	    Collection users    = FormUtils.getSelectValues(request, "assigned");

	    //did we get any validation errors?
	    checkValidation(request);

	    //get the db instance
            db = getJDODatabase();
            db.begin();

	    //load the group to process
	    Group group = (Group)db.load(Class.forName(groupClass), groupId);

	    //set the group back into request in case we get any errors
	    request.setAttribute("group", group);

	    //iterate the selected names, adding them to the group
	    //this is done 'backwards' ie, the Group is removed from the User
	    Iterator i = users.iterator();
	    while (i.hasNext() ) {
		Integer userId  = convertInteger( (String)i.next() );

		IUser user = (IUser)db.load(Class.forName(userClass), userId);

		user.removeGroup(group);
	    }

	    db.commit();

	    //success!
	    request.setAttribute("error_message", MSG_USER_REMOVED_GROUP);

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} catch (ClassNotFoundException cnfe) {
	    log.debug(groupClass + " not found", cnfe);

	    request.setAttribute("error_message", MSG_UNKNOWN_ERROR);

        } finally {
	    closeJDODatabase(db);
        }

        return nextPage;
    }


    /**
     * Dispatches <code>addUser</code> actions: edit options then forward to account info
     */
    protected String dispatchAddUser(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchAddUser *****");

        String nextPage = VIEW_MODIFY_USER;

	Database db = null;

	User editUser = null;

        try {

            //make sure user is authenticated and SU
	    IUser user = checkPermission(request, ACCESS_SUPERUSER);

	    //get data from form
	    String username     = getString("username", request);
	    String password1    = getString("pass1", request);
	    String password2    = getString("pass2", request);

	    log.debug("LoginController after getString: username: " + username);

	    //perform CSRF token test
	    checkToken(request);

	    //did we get any validation errors?
	    checkValidation(request);

	    //make sure passwords match
	    if ( !password1.equals(password2) ) {
		throw new AppException(MSG_PASSWORD_MISMATCH);
	    }

	    //get the db instance
            db = getJDODatabase();
            db.begin();

	    //check for existing username
	    OQLQuery oql = db.getOQLQuery("SELECT u FROM " + userClass + " u WHERE username LIKE $1");
            oql.bind( username );
	    QueryResults results = oql.execute();
            if ( results.hasMore() ) {
		throw new AppException(MSG_PREEXISTING_USERNAME);
	    }

	    //create the new user
	    editUser = (User)(Class.forName(userClass).newInstance());

	    //make sure password meets policy requirements
	    checkPasswordPolicy(editUser, password1);

	    log.debug("LoginController before setUserName: username: " + username);

	    editUser.setUsername(username);
	    editUser.setNewPassword(password1);
	    editUser.setCreatorId(user.getId());

	    log.debug("About to create or edit Castor user object");
	    db.create(editUser);
	    log.debug("create or edit Castor user object DONE");

	    //commit baby, commit!
	    log.debug("About to commit");
	    db.commit();

	    //success! Set new user in request
	    request.setAttribute("editUser", editUser);
	    request.setAttribute("error_message", MSG_USER_ADDED);

        } catch (AppException se) {
            request.setAttribute("add_error", se.getMessage());
            nextPage = VIEW_ADMIN_USERS;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

	} catch (InstantiationException ie) {
	    log.debug("Error instantiating " + userClass, ie);
	    request.setAttribute("modify_error", MSG_UNKNOWN_ERROR);

	} catch (ClassNotFoundException cnfe) {
	    log.debug(userClass + " not found", cnfe);
	    request.setAttribute("modify_error", MSG_UNKNOWN_ERROR);

	} catch (IllegalAccessException iae) {
	    log.debug(userClass, iae);
	    request.setAttribute("modify_error", MSG_UNKNOWN_ERROR);

        } finally {
	    closeJDODatabase(db);

        }

        return nextPage;
    }


    /**
     * Dispatches <code>modifyUser</code> actions: edit options then forward to account info
     */
    protected String dispatchModifyUser(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	log.debug("***** dispatchModifyUser *****");

	Database db = null;
	
	User editUser = null;
	String nextPage;

	try {
	    //Get returnPage from validator and set forwarding after success
	    nextPage = getReturnPage(request, VIEW_ADMIN_USERS);
		
	    //get the user id to edit from request
	    Integer editUserId = getInteger("user_id", request);
	    
	    //make sure user is authenticated
	    IUser user = checkLogin(request, "/login?acton=modifyUser&user_id=" + editUserId);

	    log.debug("lc2.0 1");

	    //if there is no user id entered, user will edit self
	    if (Utils.isEmpty(editUserId)) {
		editUserId = user.getId();
	    } 

	    log.debug("lc2.0 2");

	    //make sure user is superuser or is only editing self
	    if ( !user.isSuperuser() && !editUserId.equals(user.getId()) ) {
		throw new AccessException("");
	    }

	    log.debug("lc2.0 3");

	    //get the db instance
	    db = getJDODatabase();
	    db.begin();

	    log.debug("lc2.0 4");

	    editUser = (User)db.load(Class.forName(userClass), editUserId);
	    if (editUser == null) { 
		throw new AppException(MSG_INVALID_USERNAME);
	    }

	    log.debug("lc2.0 5");	    

	    //see if they actually pressed a submit button
	    //if this is the first request to the page, marshal user to request and forward
	    String submit       = request.getParameter("submit");
	    if (Utils.isEmpty(submit)) {
		nextPage = VIEW_MODIFY_USER;

	    } else {
		//this is an actual form submission
		
		log.debug("lc2.0 6 : in submit");	    

		//get params from form
                String username     = getString("username", request);
                String password1    = getString("pass1", request);
                String password2    = getString("pass2", request);
                String prefix       = getString("prefix", request);
                String firstName    = getString("firstName", request);
                String middleName   = getString("middleName", request);
                String lastName     = getString("lastName", request);
                String suffix       = getString("suffix", request);
                String title        = getString("title", request);
                String company      = getString("company", request);
                String email        = getString("email", request);
                String phone        = getString("phone", request);
                String extension    = getString("extension", request);
                String fax          = getString("fax", request);
                String mobile       = getString("mobile", request);
                String address1     = getString("address1", request);
                String address2     = getString("address2", request);
                String address3     = getString("address3", request);
                String city         = getString("city", request);
                String state        = getString("state", request);
                String zip          = getString("zipCode", request);
                String country      = getString("country", request);

                //admin fields

                Collection groups   = FormUtils.getSelectValues(request, "groupId");
                Boolean online      = getBoolean("online", request);
                Date expires        = getDate("expires", request);

		//Date expires        = FormUtils.getCalendarDate(request, "expires");

		//perform CSRF token test
		checkToken(request);

		//did we get any validation errors?
                checkValidation(request);

		log.debug("lc2.0 7 : in submit");	    

		//if new passwords are entered, make sure they match
		boolean updatePasswords = false;
		if ( Utils.isEmpty(password1) && Utils.isEmpty(password2) ) {
		    //both empty - don't update
		} else if ( !StringUtils.isEqual(password1, password2) ) {
		    //not equal - error
		    throw new AppException( MSG_PASSWORD_MISMATCH );
		} else {
		    //passwords match, check them against policy
		    checkPasswordPolicy(editUser, password1);
		    updatePasswords = true;
		}

		log.debug("lc2.0 8");

		//make sure email address is valid (done in validator)
		//checkValidEmail(email);
		
		editUser.setUsername(username);
		if (updatePasswords) editUser.setNewPassword(password1);

		editUser.setPrefix(prefix);
		editUser.setFirstName(firstName);
		editUser.setMiddleName(middleName);
		editUser.setLastName(lastName);
		editUser.setSuffix(suffix);
		editUser.setTitle(title);
		editUser.setCompany(company);
		editUser.setEmail(email);
		editUser.setPhone(phone);
		editUser.setExtension(extension);
		editUser.setFax(fax);
		editUser.setMobile(mobile);
		editUser.setAddress1(address1);
		editUser.setAddress2(address2);
		editUser.setAddress3(address3);
		editUser.setCity(city);
		editUser.setState(state);
		editUser.setZipCode(zip);
		editUser.setCountry(country);

		if (user.isSuperuser()) {
		    editUser.setOnline(online);
		    editUser.setDateExpires(expires);

		    Iterator i = groups.iterator();
                    Vector newGroups = new Vector();
                    while (i.hasNext()) {
                        Integer groupId = convertInteger( (String)i.next(), true );
			//                        log.debug("GROUP: " + groupId);
                        if ( groupId == null ) continue;

                        Group g = (Group)db.load(Class.forName(groupClass), groupId);
                        newGroups.add(g);
                    }
                    editUser.setGroups(newGroups);
		}

		editUser.setDateModified(new Date());

		log.debug("6");
		db.commit();
		log.debug("7");

		//JMm - if user editing their own profile, we need the session to be updated!
		//reset session if edit user == current user
		//		log.debug("USER CHECK: " + editUser.getId() + "|" + editUser.getOnline() + "|" + editUser.isAuthenticated());
		if (editUser.getId().equals(user.getId())) {

		    HttpSession session = request.getSession();
		    User sessionUser = (User)session.getAttribute( "user" );
		    
		    sessionUser.setPrefix(prefix);
		    sessionUser.setFirstName(firstName);
		    sessionUser.setMiddleName(middleName);
		    sessionUser.setLastName(lastName);
		    sessionUser.setSuffix(suffix);
		    sessionUser.setTitle(title);
		    sessionUser.setCompany(company);
		    sessionUser.setEmail(email);
		    sessionUser.setPhone(phone);
		    sessionUser.setExtension(extension);
		    sessionUser.setFax(fax);
		    sessionUser.setMobile(mobile);
		    sessionUser.setAddress1(address1);
		    sessionUser.setAddress2(address2);
		    sessionUser.setAddress3(address3);
		    sessionUser.setCity(city);
		    sessionUser.setState(state);
		    sessionUser.setZipCode(zip);
		    sessionUser.setCountry(country);

		    session.setAttribute("user",  sessionUser );
		}
	
		//TODO - do we also need to clear the cache?

		//log.debug( "Cache clear: " + editUser.getId() + "|" + editUser.getAddress().getId() );
		//CacheManager cm = db.getCacheManager();
		//cm.expireCache(User.class, editUser.getId());
		//cm.expireCache(Address.class, editUser.getAddress().getId());

		//success! go to main accounts page
		request.setAttribute("modify_error", MSG_USER_UPDATED);
		
		if (user.isSuperuser()) {
		    nextPage = VIEW_ADMIN_USERS;
		} else {
		    nextPage = VIEW_MODIFY_USER;
		}
		
		log.debug("9");

		/*
		//JMM 5/17/07 - allows forwarding to secondary page
		nextPage = getNextPage(request. VIEW_MODIFY_USER);
		*/
	    }		
	    
	} catch (AppException se) {
	    request.setAttribute("modify_error", se.getMessage());
	    nextPage = VIEW_MODIFY_USER;

	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);
	    nextPage = VIEW_MODIFY_USER;

	} catch (ClassNotFoundException cnfe) {
	    log.debug(userClass + " not found", cnfe);
            request.setAttribute("modify_error", MSG_UNKNOWN_ERROR);
	    nextPage = VIEW_MODIFY_USER;

	} finally {
	    //marshal edit editUser to request
	    request.setAttribute("editUser", editUser);
	    
	    closeJDODatabase(db);
	}	    

	return nextPage;
    }


    /**
     * Dispatches <code>login</code> actions.
     */
    protected String dispatchLogin(HttpServletRequest request, HttpServletResponse response) {

        //get the session
        HttpSession session = request.getSession();

        log.debug("***** dispatchLogin *****");

        //determine the next page to route to if successful

        Database db = null;
	String nextPage = VIEW_ROOT;

        try {

	    //see if there is a User bean already in session
	    IUser user = (IUser)session.getAttribute("user");
	    if (user == null) {
		try {
		    user = (IUser)(Class.forName(userClass).newInstance());
		    
		    //		    log.debug("Putting user in session: " + user);
		    session.setAttribute("user", user);
		} catch (Exception e) {
		    log.debug("", e);

		    throw new AppException(MSG_UNKNOWN_ERROR);
		}
	    }
	    
	    //see if they are already auth'd
	    if (!user.isAuthenticated()) {
	    
		//get request params
		String username = getString("username", request);
		String password = getString("password", request);
		nextPage = getStartIn(request, VIEW_ROOT);
		
		log.debug("LOGIN CHECK: username: " + username);
		
                //did we get any validation errors?
                checkValidation(request);

		//populate bean from DB
		db = getJDODatabase();
		db.begin();
		
		OQLQuery oql = db.getOQLQuery("SELECT u FROM " + userClass + " u WHERE username = $1");
		oql.bind( username );
		QueryResults results = oql.execute();
		if ( results.hasMore() ) {
		    user = (IUser)results.next();
		} else {
		    throw new AppException(MSG_INVALID_USERNAME);
		}
		
		//check for disabled user 
		if ( !user.getOnline().booleanValue() ) {
		    throw new AppException(MSG_USER_DISABLED);
		}
		
		//check for expired account
		if ( user.getDateExpires() != null && ( System.currentTimeMillis() > user.getDateExpires().getTime() ) ) {
		    throw new AppException(MSG_USER_EXPIRED);
		}
		
		//try to authenticate
		if (!user.authenticate(password)) {
		    throw new AppException(MSG_INVALID_PASSWORD);
		}
		
		//set the last login time
		user.setLastLogin(new Date());
		
		db.commit();
		
		session.setAttribute("user", user);
		
		log.debug("Login complete. Forwarding to: " + nextPage);

	    }
		
	} catch (AppException se) {
	    log.debug("", se);
	    request.setAttribute("error_message", se.getMessage());
	    
	    nextPage = VIEW_LOGIN;
	    
	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);
	    
	    nextPage = VIEW_LOGIN;

	} finally {
	    closeJDODatabase(db);

	}
	
	log.debug("CONTROLLER ERROR: " + (String)request.getAttribute("error_message"));
	
	return nextPage;
    }


    /**
     * Dispatches <code>logout</code> actions by invalidating the session and unbinding any attached Objects.
     */
    protected String dispatchLogout(HttpServletRequest request, HttpServletResponse response) {

	log.debug("***** dispatchLogout *****");

	String nextPage = "/";

	try {
	    //figure next page
	    //the start_in param is an anachronism -modern CodeMagi controllers use the standard "returnPage"
	    //If start_in is set go there, otherwise home page
	    nextPage = getStartIn(request, "/");

	    //did we get any validation errors?
	    checkValidation(request);

	    //invalidate the session
	    HttpSession session = request.getSession();
	    if (session != null) session.invalidate();

	} catch (AppException se) {
            log.debug("", se);

            request.setAttribute("error_message", se.getMessage());
	}

	return nextPage;
    }


    /**                                                                                      
     * Changes the current user (a super user) to some other user.                                                        
     */
    public String dispatchSwitchUser(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchSwitchUser() *****");

        //next page if successful                                                                                
        String nextPage = VIEW_MAIN;

        //get session                                                                                                 
        HttpSession session = request.getSession();

        Database db = null;

        try {
            //check for permissions                                                                             
            checkPermission(request, ACCESS_SUPERUSER);
            Integer newUserId = getInteger("userId", request);

	    //perform CSRF token test
	    //checkToken(request);

	    //did we get any validation errors?
	    checkValidation(request);

            db = getJDODatabase();
            db.begin();

            User newUser = (User)db.load(Class.forName(userClass), newUserId, Database.ReadOnly);

	    if (newUser == null) {
		request.setAttribute("error_message", "No such userId: " + newUserId);
		return VIEW_NONE;
	    }

            newUser.setNewPassword("blahblah");  //this authenticates the user
            session.setAttribute("user", newUser);
            request.setAttribute("error_message", "Changed user to " + newUser.getName());

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

        } catch (ClassNotFoundException cnfe) {
            log.debug(groupClass + " not found", cnfe);
            request.setAttribute("error_message", MSG_UNKNOWN_ERROR);

        } finally {
            closeJDODatabase(db);

        }

        return nextPage;
    }


    //UTILITY METHODS --------------------------------------------------------------------

    /**
     * Checks the input password against the password policy for this controller
     */
    protected void checkPasswordPolicy(IUser user, String password) 
	throws AppException {

	for (IPasswordPolicy policy : passwordPolicyChecks) {
	    policy.check(user, password);
	}
    }


    /**
     * Returns a User object when passed an email address. This method throws 
     * an AppException if the email address is not found in the database.
     */
    public static User getUserByEmail(String email, Database db)
        throws AppException, PersistenceException {

	return getUserByEmail(email, db, false);

    }


    /**
     * Returns a User object when passed an email address.
     *
     * @param email   The email address to search for
     * @param db      Database instance to use
     * @param create  If true, this method will attempt to create the new user
     */
    public static User getUserByEmail(String email, Database db, boolean create)
        throws AppException, PersistenceException {

	log.debug("getUserByEmail() email: " + email);

        if (db == null) throw new AppException(MSG_DATABASE_ERROR);

	email = StringUtils.trim(email);

	//load database                                                                                 
	if (!db.isActive()) db.begin();
	
	//check for existing user                               
	User user = null;
	OQLQuery oql = db.getOQLQuery("SELECT u FROM com.codemagi.login.model.User u WHERE u.email = $1");
	oql.bind( email );
	QueryResults results = oql.execute();
	if ( results.hasMore() ) {
	    user = (User)results.next();
	    
	} else if (create) {
	    user = new User();
	    user.setUsername(email);
	    user.setEmail(email);

	    //create a new random password
	    RandPass gen    = new RandPass();  //default non-confusing alphabet
	    String password = gen.getPass();   //default length (8)

	    user.setPassword(password);

	    //set the force password change bit        
	    user.setUpdateRequired( true );

	    db.create(user);

	} else {
	    //user does not exist, and we have not specified to create a new user, throw exception
	    throw new AppException(MSG_INVALID_USER);
	}
	
	return user;

    }

}
